﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Globalization;
using System.Collections.Generic;

namespace AllTalkProxy
{
    /// <summary>
    /// Handles creating the connection with the XMPP server. The includes setting up the TLS stream
    /// and authenticating the user.
    /// </summary>
    public class ConnectionManager : IDisposable
    {
        private string _server;
        private int _port;
        private string _username;
        private string _password;

        private Stream _stream;
        private StreamWriter _writer;
        private TcpClient _client;

        private Jid _userJid;

        internal EventWaitHandle _connectionCompletedEvent;
        internal EventWaitHandle DataReceivedEvent { get; private set; }
        
        private Listener _listener;

        private Queue<string> _presenceQueue;
        private Queue<string> _messageQueue;
        private Queue<Vcard> _vcardQueue;

        private Exception _exception;

        internal List<string> GetPresence()
        {
            if (_presenceQueue.Count > 0)
            {
                List<string> presenceList = new List<string>();
                lock (_presenceQueue)
                {
                    while (_presenceQueue.Count > 0)
                    {
                        presenceList.Add(_presenceQueue.Dequeue());
                    }
                    return presenceList;
                }
            }
            return new List<string>();
        }

        internal List<string> GetMessages()
        {
            if (_messageQueue.Count > 0)
            {
                List<string> messageList = new List<string>();
                lock (_messageQueue)
                {
                    while (_messageQueue.Count > 0)
                    {
                        messageList.Add(_messageQueue.Dequeue());
                    }
                    return messageList;
                }
            }
            return new List<string>();
        }

        public Listener Listener
        {
            get { return _listener; }
            set { _listener = value; }
        }
        
        public Jid Jid
        {
            get
            {
                return _userJid;
            }
        }
        public ConnectionManager(string server, int port, string username, string password)
        {
            _server = server;
            _port = port;
            _username = username;
            _password = password;
            _presenceQueue = new Queue<string>();
            _messageQueue = new Queue<string>();
            _vcardQueue = new Queue<Vcard>();
            _connectionCompletedEvent = new EventWaitHandle(/*initialState*/ false, EventResetMode.AutoReset, "BindCompletedEvent");
            DataReceivedEvent = new EventWaitHandle(/*initialState*/ false, EventResetMode.AutoReset, "MessageReceivedEvent");
        }

        private void MessageReceived(object sender, MessageEventArgs e)
        {
            lock (_messageQueue)
            {
                _messageQueue.Enqueue(e.Xml);
            }
            DataReceivedEvent.Set();
        }
        public Jid Connect()
        {
            _client = new TcpClient();
            _client.ReceiveBufferSize = Constants.ReceiveBufferSize;
            _client.ReceiveTimeout = Constants.ReceiveTimeout;
            _client.Connect(_server, _port);
            
            _stream = _client.GetStream();
            _writer = new StreamWriter(_stream) { AutoFlush = true };

            SendHeader(/*readResponse=*/ true);

            EstablishTls();

            _listener = new Listener(_stream);
            
            _listener.JidAvaiable += new EventHandler<JidEventArgs>(
                (object o, JidEventArgs j) =>
                {
                    _exception = j.Exception;
                    _userJid = j.Jid;
                    _connectionCompletedEvent.Set();
                });

            _listener.PresenceChanged += new EventHandler<PresenceEventArgs>(PresenceAdded);
            _listener.MessageReceived += new EventHandler<MessageEventArgs>(MessageReceived);
            _listener.VcardAvailable += new EventHandler<VCardEventArgs>(VcardAvailable);

            _listener.BeginListen();

            SendHeader();

            Authenticate();

            SendHeader();

            BindJid();

            EstablishSession();

            if (!_connectionCompletedEvent.WaitOne(Constants.BindTimeout))
            {
                throw new TimeoutException("Binding did not complete.");
            }

            if (_exception != null)
            {
                _listener.EndListen();
                throw _exception;
            }

            Debug.Assert(_userJid != null);

            return _userJid;
        }

        public void GetRooster(out string rosterXmlResult)
        {
            string rosterXml = null;
            _listener.RosterLoaded += new EventHandler<RosterLoadedEventArgs>(
                (object o, RosterLoadedEventArgs eventArgs) =>
                {
                    rosterXml = eventArgs.Xml;
                    _connectionCompletedEvent.Set();
                });

            SendRoosterRequest();
            SendPresence();

            if (!_connectionCompletedEvent.WaitOne(Constants.BindTimeout))
            {
                throw new TimeoutException("Binding did not complete.");
            }
            rosterXmlResult = rosterXml;
        }

        public void GetVCard(string email)
        {
            string vcardMessage = @"<iq from=""{0}"" to=""{1}"" type=""get"" id=""vc2"">"
                + @"<vCard xmlns=""vcard-temp""/></iq>";
            Send(String.Format(CultureInfo.InvariantCulture, vcardMessage, _userJid.Value, email));
        }

        private void VcardAvailable(object o, VCardEventArgs eventArgs)
        {
            lock (_vcardQueue)
            {
                _vcardQueue.Enqueue(new Vcard { UserId = eventArgs.UserId, Xml = eventArgs.Xml });
            }
            DataReceivedEvent.Set();
        }

        private void PresenceAdded(object o, PresenceEventArgs eventArgs)
        {
            lock (_presenceQueue)
            {
                _presenceQueue.Enqueue(eventArgs.Xml);
            }
            DataReceivedEvent.Set();
        }

        private void SendPresence()
        {
            string presenceMessage = @"<presence><status>{0}</status></presence>";

            Send(String.Format(CultureInfo.InvariantCulture, presenceMessage, String.Empty));
        }

        private void SendRoosterRequest()
        {
            string roosterRequest = @"<iq from=""{0}"" id=""rooster_1""  type=""get"">
                                        <query xmlns=""jabber:iq:roster""/>
                                    </iq>";
            string formattedRoosterRequest = String.Format(CultureInfo.InvariantCulture, roosterRequest, _userJid.Value);
            Send(formattedRoosterRequest);
        }

        private void SendHeader(bool readResponse = false)
        {
            Send(String.Format(@"<?xml version=""1.0""?>
                <stream:stream to=""{0}"" version=""1.0"" xmlns=""jabber:client"" xmlns:stream=""http://etherx.jabber.org/streams""> ", 
                ServerConfiguration.Recepient));
            if (readResponse)
            {
                Listener.Receive(_stream, /*endToken=*/ "</stream:features>");
            }
        }

        private void EstablishTls()
        {
            Send(@"<starttls xmlns=""urn:ietf:params:xml:ns:xmpp-tls""> </starttls>");
            Listener.Receive(_stream);

            var validateServerCertificateCallback = new RemoteCertificateValidationCallback(
                delegate(object o, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
                {
                    return errors == SslPolicyErrors.None;
                });

            SslStream encryptedStream = new SslStream(_stream,
                /*leaveStreamOpen*/ false,
                validateServerCertificateCallback,
                /*localCertificateCallback*/ null,
                EncryptionPolicy.RequireEncryption);
            encryptedStream.AuthenticateAsClient("gmail.com");

            // Replace the current stream with the encrpted one
            _stream = encryptedStream;
            _writer = new StreamWriter(encryptedStream);
            _writer.AutoFlush = true;
        }

        private Jid BindJid()
        {
            string bindRequest = @"<iq id=""bind_request_1"" type=""set""> 
                                <bind xmlns=""urn:ietf:params:xml:ns:xmpp-bind""> 
                                <resource>alltalk</resource> 
                                </bind> 
                                </iq> ";
            Send(bindRequest);
            return null;
        }

        private void Authenticate()
        {
            string authenticationRequest = @"<auth xmlns=""urn:ietf:params:xml:ns:xmpp-sasl"" mechanism=""PLAIN"">{0}</auth>";
            string credentials = "\u0000{0}\u0000{1}";

            string encodedCrendentials = Convert.ToBase64String(
                Encoding.UTF8.GetBytes(String.Format(credentials, _username, _password)));
            Send(String.Format(authenticationRequest, encodedCrendentials));
        }

        private void EstablishSession()
        {
            string sessionRequest = @"<iq to=""{0}"" id=""session_id_1"" type=""set"">
                                    <session xmlns=""urn:ietf:params:xml:ns:xmpp-session""/> 
                                    </iq>";
            Send(String.Format(sessionRequest, ServerConfiguration.Recepient));
        }

        internal void Send(string request)
        {
            // Multiple threads writing at the same time can cause issues with the stream.
            // This lock ensures serialized access to the stream.
            lock (_writer)
            {
                _writer.Write(request);
                Logger.Log(request, "Request");
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool isDisposing)
        {
            if (isDisposing)
            {
                if (_listener != null)
                {
                    _listener.EndListen();
                    _listener = null;
                }
                if (_client != null)
                {
                    _client.Close();
                }
            }
        }

        internal List<Vcard> GetVcards()
        {
            if (_vcardQueue.Count > 0)
            {
                List<Vcard> vcardList = new List<Vcard>();
                lock (_vcardQueue)
                {
                    while (_vcardQueue.Count > 0)
                    {
                        vcardList.Add(_vcardQueue.Dequeue());
                    }
                    return vcardList;
                }
            }
            return new List<Vcard>();
        }
    }
}
